<template>
  <div v-if="easyFlowVisible" class="node-container">
    <div class="node-header">
      <div class="node-header-title">
        <span>{{ name }}</span>
      </div>
          <div>
            <el-button type="danger" size="mini" @click="deleteElement">
              删除
            </el-button>
            <el-button type="primary" size="mini" @click="handleClear">
              清空
            </el-button>
            <el-button type="primary" size="mini" @click="handleSave">
              保存
            </el-button>
          </div>
    </div>
    <div class="node-content">
      <div class="node-menu">
        <node-menu @addNode="addNode" ref="nodeMenu" :menuList="menuList"></node-menu>
      </div>
      <div class="node-middle">
        <div class="node-header-content">
          <div>
            <el-button type="primary" size="mini" @click="handleRun">
              运行
            </el-button>
            <el-button type="primary" size="mini" @click="handleStop">
              暂停
            </el-button>
          </div>
          <div>
            <span style="margin-right: 10px">自动触发</span>
            <a-switch v-model="autoTrigger" checked-children="开" un-checked-children="关" />
          </div>
        </div>
        <div id="efContainer" ref="efContainer" v-flowDrag>
          <template v-for="node in data.nodeList">
            <flow-node
              :id="node.id"
              :key="node.id"
              :node="node"
              :activeElement="activeElement"
              @changeNodeSite="changeNodeSite"
              @nodeRightMenu="nodeRightMenu"
              @clickNode="clickNode"
            >
            </flow-node>
          </template>
          <!-- 给画布一个默认的宽度和高度 -->
          <div class="node-canvas" />
        </div>
        <div id="nodeLog" ref="nodeLog">
        </div>
      </div>
      <!-- 右侧表单 -->
      <div class="node-props">
        <flow-node-form
          :node="node"
          @setLineLabel="setLineLabel"
          @repaintEverything="repaintEverything"
        ></flow-node-form>
      </div>
    </div>
  </div>
</template>

<script>
import draggable from "vuedraggable";
// 使用修改后的jsplumb
import { easyFlowMixin } from "@/assets/js/mixins";
import flowNode from "./node";
import nodeMenu from "./node_menu";
import FlowNodeForm from "./node_form";
import lodash from "lodash";
import "@/assets/style/workflow.less";
import { getObj, updateWorkflow } from "@/api/visual";
import { getList, runStart } from "@/api/workflow";
import "./screenlog.js"

export default {
  data() {
    return {
      name: "未命名",
      node: {},
      // jsPlumb 实例
      jsPlumb: null,
      // 控制画布销毁
      easyFlowVisible: true,
      // 控制流程数据显示与隐藏
      flowInfoVisible: false,
      // 是否加载完毕标志位
      loadEasyFlowFinish: false,
      flowHelpVisible: false,
      autoTrigger: false,
      // 数据
      data: {},
      // 激活的元素、可能是节点、可能是连线
      activeElement: {
        // 可选值 node 、line
        type: undefined,
        // 节点ID
        nodeId: undefined,
        // 连线ID
        sourceId: undefined,
        targetId: undefined,
        uuids: undefined
      },
      zoom: 0.5,
      menuList: [],
      configId: ""
    };
  },
  // 一些基础配置移动该文件中
  mixins: [easyFlowMixin],
  components: {
    draggable,
    flowNode,
    nodeMenu,
    FlowNodeForm
  },
  directives: {
    flowDrag: {
      bind(el, binding, vnode, oldNode) {
        if (!binding) {
          return;
        }
        el.onmousedown = e => {
          if (e.button == 2) {
            // 右键不管
            return;
          }
          //  鼠标按下，计算当前原始距离可视区的高度
          let disX = e.clientX;
          let disY = e.clientY;
          el.style.cursor = "move";

          document.onmousemove = function(e) {
            // 移动时禁止默认事件
            e.preventDefault();
            const left = e.clientX - disX;
            disX = e.clientX;
            el.scrollLeft += -left;

            const top = e.clientY - disY;
            disY = e.clientY;
            el.scrollTop += -top;
          };

          document.onmouseup = function(e) {
            el.style.cursor = "auto";
            document.onmousemove = null;
            document.onmouseup = null;
          };
        };
      }
    }
  },
  mounted() {
    this.jsPlumb = jsPlumb.getInstance();
    this.getMenuList()
    this.$nextTick(() => {
      getObj({
        ID: this.id,
        VisualType: "Workflow"
      }).then(res => {
        if (res.data.code === 200) {
          this.configId = res.data.data.config.id;
          let detail = JSON.parse(res.data.data.config.detail);
          this.name = res.data.data.visual.title;
          this.autoTrigger = res.data.data.config.autoTrigger;
          this.dataReload(detail);
        }
      });
    });
  },
  methods: {
    async getMenuList() {
      let menu = [];
      let list = await getList()
      for (const type in list) {
        let item = list[type];
        let api = {
          type: "group",
          name: type,
          open: true,
          children: []
        };
        for (let i = 0; i < item.length; i++) {
          api.children.push(
            Object.assign(
              {
                type: item[i].name,
                name: item[i].name,
                ico: "el-icon-video-play"
              },
              item[i]
            )
          );
        }
        menu.push(api);
      }
      this.menuList = menu;
    },
    async handleRun() {
      screenLog.log("运行工作流")
      let data = this.generateData()

      // console.log(data);
      // let strs = JSON.stringify(data)
      // var link = document.createElement('a');
      // link.style.display = 'none';
      // document.body.appendChild(link); // Firefox workaround, see #6594
      // // link.href = URL.createObjectURL(new Blob([data], { type: 'application/json' }));
      // link.href = URL.createObjectURL(new Blob([strs], { type: 'text/plain' }));
      // link.download = 'data.json';
      // link.click();
      // return
      
      let result = await runStart(data)
      if(result.download){

        var link = document.createElement('a');
        link.style.display = 'none';
        document.body.appendChild(link); // Firefox workaround, see #6594

        let str = JSON.stringify(result.data)
        // link.href = URL.createObjectURL(new Blob([result], { type: 'application/octet-stream' }));
        link.href = URL.createObjectURL(new Blob([str], { type: 'text/plain' }));
        link.download = 'workflow.gltf';
        link.click();
      }
      this.$message.success("运行成功！");
      screenLog.log("运行成功！")
    },
    async handleStop() {
      screenLog.log("暂停工作流")
      this.$message.success("已暂停");
    },
    handleSave() {
      let data = this.generateData()
      updateWorkflow({
        id: this.id,
        configId: this.configId,
        detail: this.data,
        autoTrigger: this.autoTrigger,
        requestData: data,
        VisualType: "Workflow"
      }).then(res => {
        if (res.data.code === 200) {
          this.$message.success("保存成功！");
          screenLog.log("保存成功！")
        }
      });
    },
    generateData(){
      const nullNode = -999
      const {nodeList, lineList} = this.data

      if(nodeList.length === 0) return {}

      //多个输出的结点
      let manyOutputNodes = []
      //多个输入的结点
      let manyInputNodes = []
      //找出目标结点，找出有多个输入输出的结点
      let targetsNodes = lineList.map(line => {
        let lineSource = line.uuids[0].split('-')
        let lineTarget = line.uuids[1].split('-')
        if(lineSource[2] !== '0') {
          manyOutputNodes.push(lineSource[0])
        }
        if(lineTarget[2] !== '0') {
          manyInputNodes.push(lineTarget[0])
        }
        return lineTarget[0]
      })
      manyOutputNodes = [...new Set(manyOutputNodes)]
      manyInputNodes = [...new Set(manyInputNodes)]
      //从结点列表中根据目标结点排除不是目标结点获得顶层结点
      let topNodes = []
      let nodes = nodeList.filter((node, index) => {
        node.order = index
        let top = targetsNodes.filter(target => target === node.id)
        top.length === 0 ? topNodes.push(node) : null
        return top.length !== 0
      })
      let tu = []
      topNodes.forEach(n => {
        let array = new Array(nodeList.length).fill(nullNode)
        array[n.order] = 0
        this.travelNodes(n, nodes, array, 1)
        tu.push(array)
      })

      let max = tu[0][0], index = 0
      for (let i = 1; i < tu[0].length; i++) {
        if(tu[0][i] > max){
          max = tu[0][i]
          index = i
        }
      }

      let maxArray = tu[0][index], indexArray = 0
      for (let i = 1; i < tu.length; i++) {
        if(tu[i][index] > maxArray){
          maxArray = tu[i][index]
          indexArray = i
        }
      }
      for (let i = 0; i < tu[indexArray].length; i++) {
        let element = tu[indexArray][i]
        if(element === nullNode){
          for (let j = 0; j < tu.length; j++) {
            if( j === indexArray) continue
            let cha = maxArray - tu[j][index]
            if(element - tu[j][i] !== 0){
              tu[indexArray][i] = cha + tu[j][i]
              break
            }
          }
        }
      }

      return nodeList.map((node, index) => ({
        name: node.type,
        level: tu[indexArray][index],
        property:node.property
      }))
      // const str = JSON.stringify(data, null, 2)

      // var link = document.createElement('a');
      // link.style.display = 'none';
      // document.body.appendChild(link);

      // link.href = URL.createObjectURL(new Blob([str], { type: 'text/plain' }));
      // link.download = 'tu.json';
      // link.click();
    },
    travelNodes(node, nodes, array, level){
      if(nodes.length === 0) return

      const {lineList} = this.data
      let newNode
      lineList.forEach(line => {
        let [source, target] = [line.uuids[0].split('-')[0], line.uuids[1].split('-')[0]]
        for (let i = 0; i < nodes.length; i++) {
          const n = nodes[i];
          if(n.id === target && node.id === source) {
            array[n.order] = level
            newNode = n
            break
          }
        }
      })

      if(newNode){
        this.travelNodes(newNode, nodes.filter(n => n.id !== newNode.id), array, level + 1)
      }
    },
    handleClear() {
      this.data.lineList = []
      this.data.nodeList = []
      this.jsPlumb.deleteEveryConnection();
      this.jsPlumb.deleteEveryEndpoint();
      screenLog.warn("清空")
    },
    // 返回唯一标识
    getUUID() {
      return Math.random()
        .toString(36)
        .substr(3, 10);
    },
    jsPlumbInit() {
      const {nodeList} = this.data
      this.jsPlumb.ready(() => {
        // 导入默认配置
        this.jsPlumb.importDefaults(this.jsplumbSetting);
        // 会使整个jsPlumb立即重绘。
        this.jsPlumb.setSuspendDrawing(false, true);
        // 初始化节点
        this.loadEasyFlow();

        // 单点击了连接线, https://www.cnblogs.com/ysx215/p/7615677.html
        this.jsPlumb.bind("click", (conn, originalEvent) => {
          this.activeElement.type = "line";
          this.activeElement.uuids = conn.getUuids();
          this.activeElement.sourceId = conn.sourceId;
          this.activeElement.targetId = conn.targetId;
          // this.$refs.nodeForm.lineInit({
          //   uuids,
          //   label: conn.getLabel()
          // });
        });
        // 连线
        this.jsPlumb.bind("connection", (evt) => {
          let uuids = evt.connection.getUuids()
          if (this.loadEasyFlowFinish) {
            this.data.lineList.push({uuids});
          }
        });

        // 删除连线回调
        this.jsPlumb.bind("connectionDetached", evt => {
          let uuids = evt.connection.getUuids()
          this.deleteLine(uuids);
        });

        // 改变线的连接节点
        this.jsPlumb.bind("connectionMoved", evt => {
          this.changeLine(evt.originalSourceId, evt.originalTargetId);
        });

        // 连线右击
        this.jsPlumb.bind("contextmenu", evt => {
          // console.log("contextmenu", evt);
        });

        this.jsPlumb.bind("beforeDrop", evt => {
          const {sourceId, targetId} = evt
          const sourceNode = nodeList.find(node => node.id === sourceId)
          const targetNode = nodeList.find(node => node.id === targetId)
          let find = sourceNode.connectNode.find(node => node.id === targetNode.name)
          if(!find){
            this.$message.error("节点不支持连接此目标！");
            screenLog.error('节点不支持连接此目标！');
            return false;
          }
          let uuids = evt.connection.getUuids()
          let from = uuids[0];
          let to = uuids[1];
          if (from === to) {
            this.$message.error("节点不支持连接自己！");
            screenLog.error('节点不支持连接自己！');
            return false;
          }
          if (this.hasLine(from, to)) {
            this.$message.error("该关系已存在,不允许重复创建！");
            screenLog.error('该关系已存在,不允许重复创建！');
            return false;
          }
          if (this.hashOppositeLine(from, to)) {
            this.$message.error("不支持两个节点之间连线回环");
            screenLog.error('不支持两个节点之间连线回环！');
            return false;
          }
          this.$message.success("连接成功");
          screenLog.log('连接成功！');
          return true;
        });

        // beforeDetach
        this.jsPlumb.bind("beforeDetach", evt => {
          // console.log("beforeDetach", evt);
        });
        this.jsPlumb.setContainer(this.$refs.efContainer);

        //添加日志
        screenLog.init({
          container: this.$refs.nodeLog
        })
        screenLog.log('工作流加载成功！');
      });
    },
    // 加载流程图
    loadEasyFlow() {
      // 初始化节点
      for (let i = 0; i < this.data.nodeList.length; i++) {
        let node = this.data.nodeList[i];

        this.addEndpoint(node)
        if (!node.viewOnly) {
          this.jsPlumb.draggable(node.id, {
            containment: "parent",
            stop: function(el) {
              // 拖拽节点结束后的对调
              //   console.log("拖拽结束: ", el);
            }
          });
        }
      }
      // 初始化连线
      for (let i = 0; i < this.data.lineList.length; i++) {
        let line = this.data.lineList[i];
        let connParam = {
          uuids: line.uuids,
          label: line.label ? line.label : "",
          connector: line.connector ? line.connector : "",
          anchors: line.anchors ? line.anchors : undefined,
          paintStyle: line.paintStyle ? line.paintStyle : undefined
        };
        this.jsPlumb.connect(connParam, this.jsplumbConnectOptions);
      }
      this.$nextTick(function() {
        this.loadEasyFlowFinish = true;
      });
    },
    // 设置连线条件
    setLineLabel(from, to, label) {
      let conn = this.jsPlumb.getConnections({
        source: from,
        target: to
      })[0];
      if (!label || label === "") {
        conn.removeClass("flowLabel");
        conn.addClass("emptyFlowLabel");
      } else {
        conn.addClass("flowLabel");
      }
      conn.setLabel({
        label: label
      });
      this.data.lineList.forEach(function(line) {
        if (line.from == from && line.to == to) {
          line.label = label;
        }
      });
    },
    // 删除激活的元素
    deleteElement() {
      if (this.activeElement.type === "node") {
        this.deleteNode(this.activeElement.nodeId);
      } else if (this.activeElement.type === "line") {
        this.$confirm("确定删除所点击的线吗?", "提示", {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning"
        }).then(() => {
            let conns = this.jsPlumb.getConnections({
              uuids: this.activeElement.uuids
            });
            let conn = conns.find(c => this.activeElement.sourceId === c.sourceId && this.activeElement.targetId === c.targetId)
            this.jsPlumb.deleteConnection(conn);
            screenLog.warn("删除连线")
          })
          .catch(() => {});
      }
    },
    // 删除线
    deleteLine(uuids) {
      this.data.lineList = this.data.lineList.filter(function(line) {
        if (line.uuids[0] == uuids[0] && line.uuids[1] == uuids[1]) {
          return false;
        }
        return true;
      });
    },
    // 改变连线
    changeLine(oldFrom, oldTo) {
      this.deleteLine(oldFrom, oldTo);
    },
    // 改变节点的位置
    changeNodeSite(data) {
      for (let i = 0; i < this.data.nodeList.length; i++) {
        let node = this.data.nodeList[i];
        if (node.id === data.nodeId) {
          node.left = data.left;
          node.top = data.top;
        }
      }
    },
    /**
     * 拖拽结束后添加新的节点
     * @param evt
     * @param nodeMenu 被添加的节点对象
     * @param mousePosition 鼠标拖拽结束的坐标
     */
    addNode(evt, nodeMenu) {
      let screenX = evt.originalEvent.clientX,
        screenY = evt.originalEvent.clientY;
      let efContainer = this.$refs.efContainer;
      let containerRect = efContainer.getBoundingClientRect();
      let left = screenX,
        top = screenY;
      // 计算是否拖入到容器中
      if (
        left < containerRect.x ||
        left > containerRect.width + containerRect.x ||
        top < containerRect.y ||
        containerRect.y > containerRect.y + containerRect.height
      ) {
        this.$message.error("请把节点拖入到画布中");
        return;
      }
      left = left - containerRect.x + efContainer.scrollLeft;
      top = top - containerRect.y + efContainer.scrollTop;
      // 居中
      left -= 85;
      top -= 16;
      let node = lodash.merge(lodash.cloneDeep(nodeMenu), {
        id: this.getUUID(),
        left: left + "px",
        top: top + "px",
      });
      /**
       * 这里可以进行业务判断、是否能够添加该节点
       */
      this.data.nodeList.push(node);
      screenLog.log("添加节点：" + node.name)
      this.$nextTick(function() {
        this.addEndpoint(node)
        this.jsPlumb.draggable(node.id, {
          containment: "parent",
          stop: function(el) {
            // 拖拽节点结束后的对调
            // console.log("拖拽结束: ", el);
          }
        });
      });
    },
    addEndpoint(node){
        let number = node.outputNumber;
        let space = 1 / (number + 1);
        for (let i = 0; i < number; i++) {
          this.jsPlumb.addEndpoint(node.id, this.sourceEndpoint, {
            anchor: [space * (i + 1), 1, 0, 1],
            uuid: node.id + '-source-' + i,
          })
        }
        number = node.inputNumber;
        space = 1 / (number + 1);
        for (let i = 0; i < number; i++) {
          this.jsPlumb.addEndpoint(node.id, this.targetEndpoint, {
            anchor: [space * (i + 1), 0, 0, -1],
            uuid: node.id + '-target-' + i,
          })
        }
    },
    /**
     * 删除节点
     * @param nodeId 被删除节点的ID
     */
    deleteNode(nodeId) {
      this.$confirm("确定要删除节点" + nodeId + "?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
        closeOnClickModal: false
      })
        .then(() => {
          /**
           * 这里需要进行业务判断，是否可以删除
           */
          this.data.nodeList = this.data.nodeList.filter(function(node) {
            if (node.id === nodeId) {
              // 伪删除，将节点隐藏，否则会导致位置错位
              // node.show = false
            screenLog.warn('删除结点：' + node.name);
              return false;
            }
            return true;
          });
          this.$nextTick(function() {
            this.jsPlumb.removeAllEndpoints(nodeId);
          });
        })
        .catch(() => {});
      return true;
    },
    clickNode(node) {
      this.activeElement.type = "node";
      this.activeElement.nodeId = node.id;
      this.node = node
    //   this.$refs.nodeForm.nodeInit(this.data, nodeId);
    },
    // 是否具有该线
    hasLine(from, to) {
      for (let i = 0; i < this.data.lineList.length; i++) {
        let line = this.data.lineList[i];
        if (line.from === from && line.to === to) {
          return true;
        }
      }
      return false;
    },
    // 是否含有相反的线
    hashOppositeLine(from, to) {
      return this.hasLine(to, from);
    },
    nodeRightMenu(nodeId, evt) {
      this.menu.show = true;
      this.menu.curNodeId = nodeId;
      this.menu.left = evt.x + "px";
      this.menu.top = evt.y + "px";
    },
    repaintEverything() {
      this.jsPlumb.repaint();
    },
    // 加载流程图
    dataReload(data) {
      this.easyFlowVisible = false;
      this.data.nodeList = [];
      this.data.lineList = [];
      this.$nextTick(() => {
        data = lodash.cloneDeep(data);
        this.easyFlowVisible = true;
        this.data = data;
        this.$nextTick(() => {
          this.jsPlumb = jsPlumb.getInstance();
          this.$nextTick(() => {
            this.jsPlumbInit();
          });
        });
      });
    }
  },
  destroyed(){
    screenLog.destroy()
  },
  computed: {
    id() {
      return this.$route.params.id;
    }
  }
};
</script>


<style>
.node-container {
  height: 100vh;
  background-color: #1c2531;
}
.node-header {
  height: 40px;
  background-color: #222c3c;
  display: flex;
  padding: 0 20px;
  align-items: center;
  justify-content: space-between;
}
.node-header-title {
  color: white;
  font-size: 20px;
  font-weight: bolder;
}
.node-header-content {
  color: white;
  position: sticky;
  top: 0;
  left: 0;
  display: flex;
  padding: 0 5px;
  height: 35px;
  justify-content: space-between;
}
.node-content {
  display: flex;
  height: calc(100% - 40px);
}
.node-menu {
  width: 230px;
  border-right: 1px solid #313d4f;
  background-color: #222c3c;
  overflow-y: auto;
}

.node-middle {
    flex: 1;
    display: flex;
    flex-direction: column;
}
#nodeLog {
  height: 150px;
}
.node-canvas {
  position: absolute;
  top: 2000px;
  left: 2000px;
  width: 1px;
  height: 1px;
}
.node-props {
  width: 300px;
  border-left: 1px solid #313d4f;
}
</style>
